package com.epam.rd.mycollections;

import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Objects;

public class MyLinkedList<E> implements MyList<E>, MyQueue<E>, MyStack<E> {

    private static final String ERROR_MESSAGE = "Incorrect index ";

    private Node<E> first;
    private Node<E> last;
    private int size;

    private static class Node<E> {
        private E element;
        private Node<E> next;
        private Node<E> prev;

        public Node(E element, Node<E> prev, Node<E> next) {
            this.element = element;
            this.prev = prev;
            this.next = next;
        }
    }

    public MyLinkedList() {
    }

    public MyLinkedList(MyList<? extends E> list) {
        addAll((E[]) list.toArray());
    }

    @Override
    public void add(E el) {
        Node<E> newNode = new Node<E>(el, last, null);
        if (last == null) {
            first = newNode;
        } else {
            last.next = newNode;
        }
        last = newNode;
        size++;
    }

    @Override
    public void add(int index, E el) {
        rangeCheckAdd(index);
        if (index == size) {
            add(el);
        } else {
            Node<E> nextNode = getNode(index);
            Node<E> newNode = new Node<E>(el, nextNode.prev, nextNode);
            if (nextNode.prev == null) {
                first.prev = newNode;
                first = newNode;
            } else {
                nextNode.prev.next = newNode;
                nextNode.prev = newNode;
            }
            size++;
        }
    }

    public void addFirst(E el) {
        Node<E> newNode = new Node<E>(el, null, first);
        if (first != null) {
            first.prev = newNode;
        }
        first = newNode;
        size++;
    }

    public void addLast(E el) {
        add(el);
    }

    @Override
    public void addAll(E[] array) {
        addAll(size, array);
    }

    @Override
    public void addAll(int index, E[] array) {
        rangeCheckAdd(index);
        Node<E> nextNode;
        Node<E> prevNode;
        if (size == index) {
            nextNode = null;
            prevNode = last;
        } else {
            nextNode = getNode(index);
            prevNode = nextNode.prev;
        }
        for (E el : array) {
            Node<E> newNode = new Node<E>(el, prevNode, null);
            if (prevNode == null) {
                first = newNode;
            } else {
                prevNode.next = newNode;
            }
            prevNode = newNode;
        }
        if (nextNode == null) {
            last = prevNode;
        } else {
            prevNode.next = nextNode;
            nextNode.prev = prevNode;
        }
        size += array.length;
    }

    private void rangeCheckAdd(int index) {
        if (index < 0 || index > size) {
            throw new MyIndexOutOfBoundsException(ERROR_MESSAGE + index);
        }
    }
    
    private void rangeCheck(int index) {
        if (index < 0 || index >= size) {
            throw new MyIndexOutOfBoundsException(ERROR_MESSAGE + index);
        }
    }

    private Node<E> getNode(int index) {
        Node<E> node;
        if (size >> 1 > index) {
            node = first;
            for (int i = 0; i < index; i++) {
                node = node.next;
            }
        } else {
            node = last;
            for (int i = 0; i < size - index - 1; i++) {
                node = node.prev;
            }
        }
        return node;
    }

    @Override
    public E get(int index) {
        rangeCheck(index);
        return getNode(index).element;
    }

    public E getFirst() {
        return first.element;
    }

    public E getLast() {
        return last.element;
    }

    private E remove(Node<E> removeNode) {
        Node<E> prevNode = removeNode.prev;
        Node<E> nextNode = removeNode.next;
        if (nextNode != null) {
            nextNode.prev = prevNode;
        } else {
            last = prevNode;
        }
        if (prevNode != null) {
            prevNode.next = nextNode;
        } else {
            first = nextNode;
        }
        size--;
        return removeNode.element;
    }

    @Override
    public E remove(int index) {
        rangeCheck(index);
        Node<E> removeNode = getNode(index);
        return remove(removeNode);
    }

    public E removeFirst() {
        return remove(first);
    }

    public E removeLast() {
        return remove(last);
    }
    
    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public void clear() {
        size = 0;
        first = null;
        last = null;
    }

    @Override
    public void set(int index, E el) {
        rangeCheck(index);
        getNode(index).element = el;
    }

    @Override
    public int indexOf(Object obj) {
        int result = -1;
        Node<E> node = first;
        int i = 0;
        while (node != null) {
            if (Objects.equals(node.element, obj)) {
                result = i;
                break;
            }
            i++;
            node = node.next;
        }
        return result;
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public Object[] toArray() {
        Object[] result = new Object[size];
        Node<E> node = first;
        for (int i = 0; i < size; i++) {
            result[i] = node.element;
            node = node.next;
        }
        return result;
    }

    @Override
    public Iterator<E> iterator() {
        return new DirectIterator();
    }

    public Iterator<E> descendingIterator() {
        return new DescendingIterator();
    }

    private abstract class ListIterator implements Iterator<E> {
        private Node<E> next;
        private Node<E> lastReturned;

        @Override
        public boolean hasNext() {
            return !(next == null);
        }

        @Override
        public void remove() {
            if (lastReturned == null) {
                throw new IllegalStateException();
            }
            MyLinkedList.this.remove(lastReturned);
        }
        
        public void changeLastReturned() {
            lastReturned = next;
            if (lastReturned == null) {
                throw new NoSuchElementException();
            }
        }

        public Node<E> getNext() {
            return next;
        }

        public void setNext(Node<E> newNext) {
            this.next = newNext;
        }

        public Node<E> getLastReturned() {
            return lastReturned;
        }   
        
    }

    private class DescendingIterator extends ListIterator {

        public DescendingIterator() {
            setNext(last);
        }

        @Override
        public E next() {
            changeLastReturned();
            setNext(getNext().prev);
            return getLastReturned().element;
        }
    }

    private class DirectIterator extends ListIterator {

        public DirectIterator() {
            setNext(first);
        }

        @Override
        public E next() {
            changeLastReturned();
            setNext(getNext().next);
            return getLastReturned().element;
        }
    }

    @Override
    public void push(E el) {
        add(el);
    }

    @Override
    public E pop() {
        return removeLast();
    }

    @Override
    public void offer(E e) {
        add(e);

    }

    @Override
    public E peek() {
        return getFirst();
    }

    @Override
    public E poll() {
        return removeFirst();
    }

}
